/**
 * 
 */
package org.mips.Sirius.provider.SIRI;

import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;

import org.mips.Sirius.components.Line;
import org.mips.Sirius.components.StopMonitoring;
import org.mips.Sirius.components.StopPoint;
import org.mips.Sirius.components.Vehicle;
import org.mips.Sirius.provider.ProviderException;
import org.mips.Sirius.provider.TransportInformationProvider;
import org.mips.Sirius.provider.connection.HttpPOSTConnection;

import android.util.Log;

/**
 * @author mips Returns bogus transport information, used for debugging purposes
 */
public class SIRIProvider implements TransportInformationProvider {

	URL sourceURL;
	private HashMap<String, Line> lines;
	private ArrayList<Vehicle> vehicles;
	private ArrayList<StopPoint> stopPoints;
	private final String encoding = "UTF-8";
	private boolean boundLines = false;

	private long requestDelay = 1000;
	private long lastRequestTime = 0;
	private Object vehiclesLock = new Object();
	private Object stopPointsLock = new Object();
	private Object linesLock = new Object();

	/**
	 * Constructor
	 */
	public SIRIProvider(URL sourceURL, long requestDelay) {
		this.sourceURL = sourceURL;
		this.requestDelay = requestDelay;
	}

	private boolean updateRequestTime() {
		boolean requestAllowed = false;
		long currentTime = System.currentTimeMillis();

		if (currentTime - lastRequestTime > requestDelay) {
			requestAllowed = true;
			Log.d("debug", "Allowing provider request");
			lastRequestTime = currentTime;
		}

		return requestAllowed;
	}

	/*
	 * Returns an array of transport line names
	 */
	@SuppressWarnings("unused")
	private HashMap<String, Line> getLines() throws ProviderException {

		HashMap<String, Line> lines = null;

		// create XML line names request
		SIRILinesRequest linesRequest = new SIRILinesRequest("Sirius 1.0b");

		// create a connection object to communicate with provider
		// FIXME: don't hard-code
		HttpPOSTConnection connection = new HttpPOSTConnection(sourceURL,
				encoding);

		// send request to provider and get response
		SIRILinesDelivery linesDelivery = linesRequest.send(connection);

		ArrayList<Line> deliveredLines = linesDelivery.getLines();

		if (!deliveredLines.isEmpty()) {
			lines = new HashMap<String, Line>();

			for (Line line : deliveredLines) {
				lines.put(line.getReference(), line);
			}
		}

		return lines;
	}

	/*
	 * Returns an array of geolocated vehicles
	 */
	@SuppressWarnings("unused")
	private ArrayList<Vehicle> getVehicles() throws ProviderException {

		ArrayList<Vehicle> vehicles;

		// create xml vehicle monitoring request
		SIRIVehicleMonitoringRequest vehicleMonitoringRequest = new SIRIVehicleMonitoringRequest(
				"Sirius 1.0b");

		// create a connection object to communicate with provider
		// FIXME: don't hard-code
		HttpPOSTConnection connection = new HttpPOSTConnection(sourceURL,
				encoding);

		// send request to provider and get response
		SIRIVehicleMonitoringDelivery vehicleMonitoringDelivery = vehicleMonitoringRequest
				.send(connection);

		vehicles = vehicleMonitoringDelivery.getVehicles();

		return vehicles;
	}

	/*
	 * Returns an array of geolocated stopPoints
	 */
	@SuppressWarnings("unused")
	private ArrayList<StopPoint> getStopPoints() throws ProviderException {

		ArrayList<StopPoint> stopPoints;

		// create XML stops request
		SIRIStopPointsRequest stopPointsRequest = new SIRIStopPointsRequest(
				"Sirius 1.0b");

		// create a connection object to communicate with provider
		// FIXME: don't hard-code
		HttpPOSTConnection connection = new HttpPOSTConnection(sourceURL,
				encoding);

		// send request to provider and get response
		SIRIStopPointsDelivery stopPointsDelivery = stopPointsRequest
				.send(connection);

		stopPoints = stopPointsDelivery.getStopPoints();

		// now monitor all stops for geocoords
		// create XML stop monitoring request
		SIRIStopMonitoringRequest stopMonitoringRequest = new SIRIStopMonitoringRequest(
				"Sirius 1.0b");

		SIRIStopMonitoringDelivery stopMonitoringDelivery = stopMonitoringRequest
				.send(connection);

		ArrayList<StopPoint> monitoredStopPoints = stopMonitoringDelivery
				.getStopPoints();

		// Precondition: every monitoredStopPoint has its own Geolocation
		// coordinates
		for (StopPoint geoLocatedStop : monitoredStopPoints) {
			for (StopPoint stop : stopPoints) {
				if (stop.getReference().equals(geoLocatedStop.getReference())) {
					stop.setCoordinates(geoLocatedStop.getCoordinates());
				}
			}
		}

		return stopPoints;
	}

	/*
	 * Returns a stop monitoring
	 */
	private StopMonitoring getStopPointMonitoring(StopPoint stop)
			throws ProviderException {

		// create XML stop monitoring request
		SIRIStopMonitoringRequest stopMonitoringRequest = new SIRIStopMonitoringRequest(
				"Sirius 1.0b", stop);

		// create a connection object to communicate with provider
		// FIXME: don't hard-code
		HttpPOSTConnection connection = new HttpPOSTConnection(sourceURL,
				encoding);

		SIRIStopMonitoringDelivery stopMonitoringDelivery = stopMonitoringRequest
				.send(connection);

		Log.d("tap", "bind1");

		StopMonitoring monitoring = stopMonitoringDelivery.getStopMonitoring();

		for (Vehicle monitoredVehicle : monitoring.getLastMonitoredVehicles()) {
			monitoredVehicle.setName(lines.get(
					monitoredVehicle.getLineReference()).getName());
		}

		Log.d("tap", "bind2");

		// save last monitoring
		for (StopPoint cachedStop : stopPoints) {
			if (cachedStop.getReference() == stop.getReference()) {
				cachedStop.setLastMonitoring(monitoring);
			}
		}

		Log.d("tap", "end bind");

		return monitoring;
	}

	/*
	 * Fetches and returns vehicles information
	 */
	@Override
	public ArrayList<Vehicle> getRealtimeVehicleInformation()
			throws ProviderException {

		boolean requestAllowed = updateRequestTime();

		if (requestAllowed) {
			try {
				new Thread(new AsyncHelper(this.getClass().getDeclaredField(
						"vehicles"), this.getClass().getDeclaredMethod(
						"getVehicles"), vehiclesLock, true), "vehiclesThred")
						.start();
			} catch (Exception e) {
				e.printStackTrace();
				Log.d("debug", "Thread creation failed for getVehicles() "
						+ e.getMessage());
			}
		}

		return vehicles;
	}

	/*
	 * Fetches and returns stops information
	 */
	@Override
	public ArrayList<StopPoint> getStaticStopPointsInformation()
			throws ProviderException {

		if (stopPoints == null) {
			try {
				new Thread(new AsyncHelper(this.getClass().getDeclaredField(
						"stopPoints"), this.getClass().getDeclaredMethod(
						"getStopPoints"), stopPointsLock), "stopPointsThred")
						.start();
			} catch (Exception e) {
				e.printStackTrace();
				Log.d("debug", "Thread creation failed for getStopPoints() "
						+ e.getMessage());
			}
		}

		bindStopPointsToLines();

		return stopPoints;

	}

	/*
	 * Fetches and returns stops information
	 */
	@Override
	public synchronized StopMonitoring getRealtimeStopPointInformation(
			StopPoint stop) throws ProviderException {

		updateRequestTime();

		return getStopPointMonitoring(stop);
	}

	/*
	 * Fetches and returns lines information
	 */
	@Override
	public ArrayList<Line> getStaticLinesInformation() throws ProviderException {

		if (lines == null) {
			try {
				new Thread(new AsyncHelper(this.getClass().getDeclaredField(
						"lines"),
						this.getClass().getDeclaredMethod("getLines"),
						linesLock), "linesThred").start();
			} catch (Exception e) {
				e.printStackTrace();
				Log.d("debug", "Thread creation failed for getLines() "
						+ e.getMessage());
			}

			return null;
		}

		bindStopPointsToLines();

		return new ArrayList<Line>(lines.values());
	}

	private void bindStopPointsToLines() {
		if ((lines == null) || (stopPoints == null))
			return;

		if (boundLines)
			return;

		// Precondition: every stop has its own lineRefs
		for (StopPoint stop : stopPoints) {
			ArrayList<Line> movedLines = new ArrayList<Line>();
			for (Line line : stop.getLines()) {
				lines.get(line.getReference()).addStop(stop);
				movedLines.add(lines.get(line.getReference()));
			}
			stop.setLines(movedLines);
		}
		// Postcondition: every line has its own stops,
		// every stop has its own lines.

		Log.d("debug", "Bound stops to lines");

		boundLines = true;
	}

	private class AsyncHelper implements Runnable {

		private Field field;
		private Method method;
		private Object lock;
		private boolean overwrite;

		/**
		 * @param field
		 * @param method
		 */
		public AsyncHelper(Field field, Method method, Object lock,
				boolean overwrite) {
			super();
			this.field = field;
			field.setAccessible(true);
			this.method = method;
			method.setAccessible(true);
			this.lock = lock;
			this.overwrite = overwrite;
		}

		public AsyncHelper(Field field, Method method, Object lock) {
			this(field, method, lock, false);
		}

		@Override
		public void run() {
			Log.d("debug", "Trying async request");
			try {
				synchronized (lock) {
					Object values = field.get(SIRIProvider.this);
					if (!overwrite && (values != null))
						return;

					values = method.invoke(SIRIProvider.this);

					synchronized (SIRIProvider.this) {
						field.set(SIRIProvider.this, values);
						Log.d("debug", "AsyncHelper: setting "
								+ field.getName());
					}
				}
			} catch (Exception e) {
				e.printStackTrace();
				Log.d("debug", "Reflection failed: " + method.getName());
			}
		}
	}
}
